package link.luyu.plugin.brochain;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.reactivex.Flowable;
import java.io.IOException;
import java.math.BigInteger;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import link.luyu.plugin.brochain.types.BlockAndPage;
import link.luyu.plugin.brochain.web3.Web3;
import link.luyu.plugin.brochain.web3.methods.response.Block;
import link.luyu.plugin.brochain.web3.methods.response.Page;
import link.luyu.plugin.brochain.web3.methods.response.Votes;
import link.luyu.protocol.link.Connection;
import org.web3j.protocol.ObjectMapperFactory;
import org.web3j.protocol.Web3jService;
import org.web3j.protocol.core.*;
import org.web3j.protocol.websocket.events.Notification;

public class BrochainService {

    private final Web3 web3;

    public BrochainService(Connection connection) {
        Web3Service service = new Web3Service(connection, true);
        web3 = new Web3(service);
    }

    public Web3 web3() {
        return web3;
    }

    public CompletableFuture<BlockAndPage> getBlockAndPage(
            BigInteger blockNumber, String regionId) {
        DefaultBlockParameter blockParameter = DefaultBlockParameter.valueOf(blockNumber);
        CompletableFuture<BlockAndPage> futureBlockAndPage =
                web3.brochainGetBlock(blockParameter)
                        .sendAsync()
                        .thenCompose(
                                blockResponse -> {
                                    Optional<Block> block = blockResponse.getBlock();
                                    if (block.isPresent()) {
                                        List<Block.RegionPage> pages = block.get().getRegionPages();
                                        String pageHash = null;
                                        for (Block.RegionPage page : pages) {
                                            if (page.getRegionId().equals(regionId)) {
                                                pageHash = page.getPageHash();
                                                break;
                                            }
                                        }
                                        if (pageHash != null) {
                                            return web3.brochainGetPage(regionId, pageHash, true)
                                                    .sendAsync()
                                                    .thenApply(
                                                            (pageResponse) -> {
                                                                Optional<Page> page =
                                                                        pageResponse.getPage();
                                                                if (page.isPresent()) {
                                                                    return new BlockAndPage(
                                                                            block.get(),
                                                                            page.get());
                                                                } else {
                                                                    throw new AssertionError(
                                                                            "Page should not be null");
                                                                }
                                                            });
                                        } else {
                                            return CompletableFuture.completedFuture(
                                                    new BlockAndPage(block.get(), null));
                                        }
                                    } else {
                                        return CompletableFuture.completedFuture(null);
                                    }
                                });
        CompletableFuture<Votes> futureVotes =
                web3.brochainGetVotes(blockParameter)
                        .sendAsync()
                        .thenApply(
                                votesResponse -> {
                                    Optional<Votes> votes = votesResponse.getVotes();
                                    return votes.orElse(null);
                                });
        return CompletableFuture.allOf(futureBlockAndPage, futureVotes)
                .thenApply(
                        result -> {
                            BlockAndPage blockAndPage = futureBlockAndPage.join();
                            Votes votes = futureVotes.join();
                            if (blockAndPage != null) {
                                blockAndPage.setVotes(votes);
                            }
                            return blockAndPage;
                        });
    }

    private static class Web3Service implements Web3jService {

        private final Connection connection;

        private final ObjectMapper objectMapper;

        public Web3Service(Connection connection, boolean includeRawResponses) {
            this.connection = connection;
            objectMapper = ObjectMapperFactory.getObjectMapper(includeRawResponses);
        }

        @Override
        public <T extends Response> T send(Request request, Class<T> responseType)
                throws IOException {
            try {
                return sendAsync(request, responseType).get();
            } catch (InterruptedException | ExecutionException e) {
                throw new IOException(e.getMessage(), e);
            }
        }

        @Override
        public <T extends Response> CompletableFuture<T> sendAsync(
                Request request, Class<T> responseType) {
            CompletableFuture<T> result = new CompletableFuture<>();
            byte[] payload = null;
            try {
                payload = objectMapper.writeValueAsBytes(request);
            } catch (JsonProcessingException e) {
                result.completeExceptionally(new ExecutionException("Error processing request", e));
            }
            if (payload != null) {
                connection.asyncSend(
                        null,
                        0,
                        payload,
                        (errorCode, message, responseData) -> {
                            if (errorCode >= 200 && errorCode < 300) {
                                try {
                                    result.complete(
                                            objectMapper.readValue(responseData, responseType));
                                } catch (IOException e) {
                                    result.completeExceptionally(
                                            new ExecutionException("Error processing response", e));
                                }
                            } else {
                                result.completeExceptionally(new ExecutionException(message, null));
                            }
                        });
            }
            return result;
        }

        @Override
        public BatchResponse sendBatch(BatchRequest batchRequest) {
            throw new UnsupportedOperationException(
                    String.format(
                            "Service %s does not support batch requests",
                            this.getClass().getSimpleName()));
        }

        @Override
        public CompletableFuture<BatchResponse> sendBatchAsync(BatchRequest batchRequest) {
            throw new UnsupportedOperationException(
                    String.format(
                            "Service %s does not support batch requests",
                            this.getClass().getSimpleName()));
        }

        @Override
        public <T extends Notification<?>> Flowable<T> subscribe(
                Request request, String unsubscribeMethod, Class<T> responseType) {
            throw new UnsupportedOperationException(
                    String.format(
                            "Service %s does not support subscriptions",
                            this.getClass().getSimpleName()));
        }

        @Override
        public void close() {}
    }
}
